import logging
import sys
import time
from typing import Optional, Dict, Type, Any

import requests

from ...backend_api.utils import get_response_cls

from .response import ResponseMeta, Response
from .errors import ResultNotReadyError, TimeoutExpiredError


class CallResult(object):
    @property
    def meta(self) -> ResponseMeta:
        return self.__meta

    @property
    def response(self) -> Response:
        return self.__response

    @property
    def response_data(self) -> Optional[Dict[str, Any]]:
        return self.__response_data

    @property
    def async_accepted(self) -> bool:
        return self.meta.result_code == 202

    @property
    def request_cls(self) -> Optional[Type[Any]]:
        return self.__request_cls

    def __init__(
        self,
        meta: ResponseMeta,
        response: Response = None,
        response_data: dict = None,
        request_cls: type = None,
        session: Any = None,
    ) -> None:
        assert isinstance(meta, ResponseMeta)
        if response and not isinstance(response, Response):
            raise ValueError("response should be an instance of %s" % Response.__name__)
        elif response_data and not isinstance(response_data, dict):
            raise TypeError("data should be an instance of {}".format(dict.__name__))

        self.__meta = meta
        self.__response = response
        self.__request_cls = request_cls
        self.__session = session
        self.__async_result = None

        if response_data is not None:
            self.__response_data = response_data
        elif response is not None:
            try:
                self.__response_data = response.to_dict()
            except AttributeError:
                raise TypeError("response should be an instance of {}".format(Response.__name__))
        else:
            self.__response_data = None

    @classmethod
    def _make_raw_response(
        cls,
        request_cls: Optional[Type] = None,
        service: Optional[str] = None,
        action: Optional[str] = None,
        status_code: int = 200,
        text: Optional[str] = None,
    ) -> "CallResult":
        service = service or (request_cls._service if request_cls else "unknown")
        action = action or (request_cls._action if request_cls else "unknown")
        return cls(
            request_cls=request_cls,
            meta=ResponseMeta.from_raw_data(
                status_code=status_code,
                text=text,
                endpoint="%(service)s.%(action)s" % locals(),
            ),
        )

    @classmethod
    def from_result(
        cls,
        res: requests.Response,
        request_cls: Optional[Type] = None,
        logger: Optional[logging.Logger] = None,
        service: Optional[str] = None,
        action: Optional[str] = None,
        session: Optional[Any] = None,
    ) -> "CallResult":
        """From requests result"""
        response_cls = get_response_cls(request_cls)

        if res is None:
            return cls._make_raw_response(request_cls=request_cls, service=service, action=action)

        try:
            data = res.json()
        except ValueError:
            return cls._make_raw_response(
                request_cls=request_cls,
                service=service,
                action=action,
                status_code=res.status_code,
                text=res.text,
            )
        if "meta" not in data:
            raise ValueError("Missing meta section in response payload")
        try:
            meta = ResponseMeta(**data["meta"])
            # TODO: validate meta?
            # meta.validate()
        except Exception as ex:
            raise ValueError("Failed parsing meta section in response payload (data=%s, error=%s)" % (data, ex))

        response = None
        response_data = None
        try:
            response_data = data.get("data", {})
            if response_cls:
                response = response_cls(**response_data)
                # TODO: validate response?
                # response.validate()
        except Exception as e:
            if logger:
                logger.warning("Failed parsing response: %s" % str(e))
        return cls(
            meta=meta,
            response=response,
            response_data=response_data,
            request_cls=request_cls,
            session=session,
        )

    def ok(self) -> bool:
        return self.meta.result_code == 200

    def ready(self) -> bool:
        if not self.async_accepted:
            return True
        session = self.__session
        res = session.send_request(
            service="async",
            action="result",
            json=dict(id=self.meta.id),
            async_enable=False,
        )
        if res.status_code != session._async_status_code:
            self.__async_result = CallResult.from_result(res=res, request_cls=self.request_cls, logger=session._logger)
            return True

    def result(self) -> "CallResult":
        if not self.async_accepted:
            return self
        if self.__async_result is None:
            raise ResultNotReadyError(self._format_msg("Timeout expired"), call_id=self.meta.id)
        return self.__async_result

    def wait(
        self,
        timeout: Optional[int] = None,
        poll_interval: int = 5,
        verbose: bool = False,
    ) -> "CallResult":
        if not self.async_accepted:
            return self
        session = self.__session
        poll_interval = max(1, poll_interval)
        remaining = max(0, timeout) if timeout else sys.maxsize
        while remaining > 0:
            if not self.ready():
                # Still pending, log and continue
                if verbose and session._logger:
                    progress = (
                        "waiting forever"
                        if timeout is False
                        else "%.1f/%.1f seconds remaining" % (remaining, float(timeout or 0))
                    )
                    session._logger.info(
                        "Waiting for asynchronous call %s (%s)" % (self.request_cls.__name__, progress)
                    )
                time.sleep(poll_interval)
                remaining -= poll_interval
                continue
            # We've got something (good or bad, we don't know), create a call result and return
            return self.result()

        # Timeout expired, return the asynchronous call's result (we've got nothing better to report)
        raise TimeoutExpiredError(self._format_msg("Timeout expired"), call_id=self.meta.id)

    def _format_msg(self, msg: str) -> str:
        return msg + " for call %s (%s)" % (self.request_cls.__name__, self.meta.id)
